/* ttyio.c -  tty i/O functions
 * Copyright (C) 1997-2019 Werner Koch
 * Copyright (C) 1998-2020 Free Software Foundation, Inc.
 * Copyright (C) 2015-2020 g10 Code GmbH
 *
 * This file is part of GnuPG.
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of either
 *
 *   - the GNU Lesser General Public License as published by the Free
 *     Software Foundation; either version 3 of the License, or (at
 *     your option) any later version.
 *
 * or
 *
 *   - the GNU General Public License as published by the Free
 *     Software Foundation; either version 2 of the License, or (at
 *     your option) any later version.
 *
 * or both in parallel, as here.
 *
 * This file is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
 * SPDX-License-Identifier: (LGPL-3.0-or-later OR GPL-2.0-or-later)
 */

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>

#ifdef HAVE_TCGETATTR
# include <termios.h>
#else
# ifdef HAVE_TERMIO_H
/* simulate termios with termio */
#  include <termio.h>
#  define termios termio
#  define tcsetattr ioctl
#  define TCSAFLUSH TCSETAF
#  define tcgetattr(A,B) ioctl(A,TCGETA,B)
#  define HAVE_TCGETATTR
# endif
#endif
#ifdef HAVE_W32_SYSTEM
# ifdef HAVE_WINSOCK2_H
#  include <winsock2.h>
# endif
# include <windows.h>
# ifdef HAVE_TCGETATTR
#  error mingw32 and termios
# endif
#endif
#include <errno.h>
#include <ctype.h>

#include "util.h"
#include "ttyio.h"
#include "i18n.h"
#include "common-defs.h"

#define CONTROL_D ('D' - 'A' + 1)


#ifdef HAVE_W32_SYSTEM
static struct {
    HANDLE in, out;
} con;
#define DEF_INPMODE  (ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT    \
                                       |ENABLE_PROCESSED_INPUT )
#define HID_INPMODE  (ENABLE_LINE_INPUT|ENABLE_PROCESSED_INPUT )
#define DEF_OUTMODE  (ENABLE_WRAP_AT_EOL_OUTPUT|ENABLE_PROCESSED_OUTPUT)

#else /* Unix */
static FILE *ttyfp = NULL;
#endif /* Unix */

static int initialized;
static int last_prompt_len;
static int batchmode;
static int no_terminal;

#ifdef HAVE_TCGETATTR
    static struct termios termsave;
    static int restore_termios;
#endif

/* Hooks set by gpgrlhelp.c if required. */
static void (*my_rl_set_completer) (rl_completion_func_t *);
static void (*my_rl_inhibit_completion) (int);
static void (*my_rl_cleanup_after_signal) (void);
static void (*my_rl_init_stream) (FILE *);
static char *(*my_rl_readline) (const char*);
static void (*my_rl_add_history) (const char*);
static int (*my_rl_rw_history)(const char *, int, int);

/* This is a wrapper around ttyname so that we can use it even when
   the standard streams are redirected.  It figures the name out the
   first time and returns it in a statically allocated buffer. */
const char *
tty_get_ttyname (void)
{
  static char *name;

  /* On a GNU system ctermid() always return /dev/tty, so this does
     not make much sense - however if it is ever changed we do the
     Right Thing now. */
#ifdef HAVE_CTERMID
  static int got_name;

  if (!got_name)
    {
      const char *s;
      /* Note that despite our checks for these macros the function is
         not necessarily thread save.  We mainly do this for
         portability reasons, in case L_ctermid is not defined. */
# if defined(_POSIX_THREAD_SAFE_FUNCTIONS) || defined(_POSIX_TRHEADS)
      char buffer[L_ctermid];
      s = ctermid (buffer);
# else
      s = ctermid (NULL);
# endif
      if (s)
        name = strdup (s);
      got_name = 1;
    }
#endif /*HAVE_CTERMID*/
  /* Assume the standard tty on memory error or when there is no
     ctermid. */
  return name? name : "/dev/tty";
}



#ifdef HAVE_TCGETATTR
static void
cleanup(void)
{
  if (restore_termios)
    {
      restore_termios = 0; /* do it prior in case it is interrupted again */
      if (tcsetattr(fileno(ttyfp), TCSAFLUSH, &termsave))
        log_error ("tcsetattr() failed: %s\n", strerror (errno));
    }
}
#endif /*HAVE_TCGETATTR*/


static void
init_ttyfp(void)
{
  if (initialized)
    return;

#ifdef HAVE_W32_SYSTEM
  {
    SECURITY_ATTRIBUTES sa;

    memset (&sa, 0, sizeof(sa));
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = TRUE;
    con.out = CreateFileA ("CONOUT$", GENERIC_READ|GENERIC_WRITE,
                           FILE_SHARE_READ|FILE_SHARE_WRITE,
                           &sa, OPEN_EXISTING, 0, 0 );
    if (con.out == INVALID_HANDLE_VALUE)
      log_fatal ("open(CONOUT$) failed: %s\n", w32_strerror (-1));

    memset (&sa, 0, sizeof(sa));
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = TRUE;
    con.in = CreateFileA ("CONIN$", GENERIC_READ|GENERIC_WRITE,
                          FILE_SHARE_READ|FILE_SHARE_WRITE,
                          &sa, OPEN_EXISTING, 0, 0 );
    if (con.in == INVALID_HANDLE_VALUE)
      log_fatal ("open(CONIN$) failed: %s\n", w32_strerror (-1));
  }
  SetConsoleMode (con.in, DEF_INPMODE);
  SetConsoleMode (con.out, DEF_OUTMODE);

#else /* Unix */
  ttyfp = batchmode? stderr : fopen (tty_get_ttyname (), "r+");
  if (!ttyfp)
    {
      log_error ("cannot open '%s': %s\n", tty_get_ttyname (), strerror(errno));
      exit (2);
    }
  if (my_rl_init_stream)
    my_rl_init_stream (ttyfp);
#endif /* Unix */

#ifdef HAVE_TCGETATTR
  atexit (cleanup);
#endif

  initialized = 1;
}


int
tty_batchmode( int onoff )
{
  int old = batchmode;
  if (onoff != -1)
    batchmode = onoff;
  return old;
}

int
tty_no_terminal(int onoff)
{
  int old = no_terminal;
  no_terminal = onoff ? 1 : 0;
  return old;
}


#ifdef HAVE_W32_SYSTEM
/* Write the UTF-8 encoded STRING to the console.  */
static void
w32_write_console (const char *string)
{
  wchar_t *wstring;
  DWORD n, nwritten;

  wstring = utf8_to_wchar (string);
  if (!wstring)
    log_fatal ("w32_write_console failed: %s", strerror (errno));
  n = wcslen (wstring);

  if (!WriteConsoleW (con.out, wstring, n, &nwritten, NULL))
    log_fatal ("WriteConsole failed: %s", w32_strerror (-1));
  if (n != nwritten)
    log_fatal ("WriteConsole failed: %lu != %lu\n",
               (unsigned long)n, (unsigned long)nwritten);
  last_prompt_len += n;
  xfree (wstring);
}
#endif /*HAVE_W32_SYSTEM*/


void
tty_printf (const char *fmt, ... )
{
  va_list arg_ptr;

  if (no_terminal)
    return;

  if (!initialized)
    init_ttyfp ();

  va_start (arg_ptr, fmt);

#ifdef HAVE_W32_SYSTEM
  {
    char *buf = NULL;

    vasprintf(&buf, fmt, arg_ptr);
    if (!buf)
      log_bug ("vasprintf() failed\n");
    w32_write_console (buf);
    xfree (buf);
  }
#else /* Unix */
  last_prompt_len += vfprintf (ttyfp, fmt, arg_ptr) ;
  fflush (ttyfp);
#endif /* Unix */
  va_end(arg_ptr);
}


/* Same as tty_printf but if FP is not NULL, behave like a regular
   fprintf. */
void
tty_fprintf (estream_t fp, const char *fmt, ... )
{
  va_list arg_ptr;

  if (fp)
    {
      va_start (arg_ptr, fmt) ;
      es_vfprintf (fp, fmt, arg_ptr );
      va_end (arg_ptr);
      return;
    }

  if (no_terminal)
    return;

  if (!initialized)
    init_ttyfp ();

  va_start (arg_ptr, fmt);

#ifdef HAVE_W32_SYSTEM
  {
    char *buf = NULL;

    vasprintf (&buf, fmt, arg_ptr);
    if (!buf)
      log_bug ("vasprintf() failed\n");
    w32_write_console (buf);
    xfree (buf);
  }
#else /* Unix */
  last_prompt_len += vfprintf(ttyfp,fmt,arg_ptr) ;
  fflush(ttyfp);
#endif /* Unix */

  va_end(arg_ptr);
}


/* Print a string, but filter all control characters out.  If FP is
 * not NULL print to that stream instead to the tty.  */
static void
do_print_string (estream_t fp, const byte *p, size_t n )
{
  if (no_terminal && !fp)
    return;

  if (!initialized && !fp)
    init_ttyfp();

  if (fp)
    {
      print_utf8_buffer (fp, p, n);
      return;
    }

#ifdef HAVE_W32_SYSTEM
  /* Not so effective, change it if you want */
  for (; n; n--, p++)
    {
      if (iscntrl (*p))
        {
          if( *p == '\n' )
            tty_printf ("\\n");
          else if( !*p )
            tty_printf ("\\0");
          else
            tty_printf ("\\x%02x", *p);
        }
      else
        tty_printf ("%c", *p);
    }
#else /* Unix */
  for (; n; n--, p++)
    {
      if (iscntrl (*p))
        {
          putc ('\\', ttyfp);
          if ( *p == '\n' )
            putc ('n', ttyfp);
          else if ( !*p )
            putc ('0', ttyfp);
          else
            fprintf (ttyfp, "x%02x", *p );
        }
      else
        putc (*p, ttyfp);
    }
#endif /* Unix */
}


void
tty_print_utf8_string2 (estream_t fp, const byte *p, size_t n, size_t max_n)
{
  size_t i;
  char *buf;

  if (no_terminal && !fp)
    return;

  /* We can handle plain ascii simpler, so check for it first. */
  for(i=0; i < n; i++ )
    {
      if (p[i] & 0x80)
        break;
    }
  if (i < n)
    {
      buf = utf8_to_native ((const char *)p, n, 0);
      if (max_n && (strlen (buf) > max_n))
        buf[max_n] = 0;
      /* (utf8_to_native already did  the control character quoting) */
      tty_fprintf (fp, "%s", buf);
      xfree (buf);
    }
  else
    {
      if (max_n && (n > max_n))
        n = max_n;
      do_print_string (fp, p, n );
    }
}


void
tty_print_utf8_string (const byte *p, size_t n)
{
  tty_print_utf8_string2 (NULL, p, n, 0);
}


/* Read a string from the tty using PROMPT.  If HIDDEN is set the
 * input is not echoed.  */
static char *
do_get (const char *prompt, int hidden)
{
  char *buf;
  int  n;  /* Allocated size of BUF.  */
  int  i;  /* Number of bytes in BUF. */
  int  c;
#ifdef HAVE_W32_SYSTEM
  char *utf8buf;
  int errcount = 0;
#else
  byte cbuf[1];
#endif

  if (batchmode)
    {
      log_error (_("Sorry, we are in batchmode - can't get input\n"));
      exit (2);
    }

  if (no_terminal)
    {
      log_error (_("Sorry, no terminal at all requested - can't get input\n"));
      exit (2);
    }

  if (!initialized)
    init_ttyfp ();

  last_prompt_len = 0;
  tty_printf ("%s", prompt);
  buf = xmalloc ((n=50));
  i = 0;

#ifdef HAVE_W32_SYSTEM
  if (hidden)
    SetConsoleMode(con.in, HID_INPMODE );

  utf8buf = NULL;
  for (;;)
    {
      DWORD nread;
      wchar_t wbuf[2];
      const unsigned char *s;

      if (!ReadConsoleW (con.in, wbuf, 1, &nread, NULL))
        log_fatal ("ReadConsole failed: %s", w32_strerror (-1));
      if (!nread)
        continue;

      wbuf[1] = 0;
      xfree (utf8buf);
      utf8buf = wchar_to_utf8 (wbuf);
      if (!utf8buf)
        {
          log_info ("wchar_to_utf8 failed: %s\n", strerror (errno));
          if (++errcount > 10)
            log_fatal (_("too many errors; giving up\n"));
          continue;
        }
      if (*utf8buf == '\n')
        {
          if (utf8buf[1])
            {
              log_info ("ReadConsole returned more than requested"
                        " (0x0a,0x%02x)\n", utf8buf[1]);
              if (++errcount > 10)
                log_fatal (_("too many errors; giving up\n"));
            }
          break;
        }
      if (!hidden)
        last_prompt_len++;

      for (s=utf8buf; *s; s++)
        {
          c = *s;
          if (c == '\t')
            c = ' ';  /* Map tab to a space.  */
          else if ((c >= 0 && c <= 0x1f) || c == 0x7f)
            continue; /* Remove control characters.  */
          if (!(i < n-1))
            {
              n += 50;
              buf = xrealloc (buf, n);
            }
          buf[i++] = c;
        }
    }
  xfree (utf8buf);

  if (hidden)
    SetConsoleMode(con.in, DEF_INPMODE );

#else /* Unix */

  if (hidden)
    {
#ifdef HAVE_TCGETATTR
      struct termios term;

      if (tcgetattr(fileno(ttyfp), &termsave))
        log_fatal ("tcgetattr() failed: %s\n", strerror(errno));
      restore_termios = 1;
      term = termsave;
      term.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
      if (tcsetattr( fileno(ttyfp), TCSAFLUSH, &term ) )
        log_fatal("tcsetattr() failed: %s\n", strerror(errno));
#endif /*HAVE_TCGETATTR*/
    }

  /* fixme: How can we avoid that the \n is echoed w/o disabling
   * canonical mode - w/o this kill_prompt can't work */
  while (read(fileno(ttyfp), cbuf, 1) == 1 && *cbuf != '\n')
    {
      if (!hidden)
        last_prompt_len++;
      c = *cbuf;
      if (c == CONTROL_D)
        log_info (_("Control-D detected\n"));

      if (c == '\t') /* Map tab to a space.  */
        c = ' ';
      else if ( (c >= 0 && c <= 0x1f) || c == 0x7f)
        continue; /* Skip all other ASCII control characters.  */
      if (!(i < n-1))
        {
          n += 50;
          buf = xrealloc (buf, n);
        }
      buf[i++] = c;
    }
  if (*cbuf != '\n')
    {
      buf[0] = CONTROL_D;
      i = 1;
    }

  if (hidden)
    {
#ifdef HAVE_TCGETATTR
      if (tcsetattr (fileno(ttyfp), TCSAFLUSH, &termsave))
        log_error ("tcsetattr() failed: %s\n", strerror(errno));
      restore_termios = 0;
#endif /*HAVE_TCGETATTR*/
    }
#endif /* Unix */

  buf[i] = 0;
  return buf;
}



/* Note: This function never returns NULL. */
char *
tty_get( const char *prompt )
{
  if (!batchmode && !no_terminal && my_rl_readline && my_rl_add_history)
    {
      char *line;
      char *buf;

      if (!initialized)
	init_ttyfp();

      last_prompt_len = 0;

      line = my_rl_readline (prompt?prompt:"");

      /* We need to copy it to memory controlled by our malloc
         implementations; further we need to convert an EOF to our
         convention. */
      buf = xmalloc(line? strlen(line)+1:2);
      if (line)
        {
          strcpy (buf, line);
          trim_spaces (buf);
          if (strlen (buf) > 2 )
            my_rl_add_history (line); /* Note that we test BUF but add LINE. */
          free (line);
        }
      else
        {
          buf[0] = CONTROL_D;
          buf[1] = 0;
        }
      return buf;
    }
  else
    return do_get ( prompt, 0 );
}


/* Variable argument version of tty_get.  The prompt is actually a
 * format string with arguments.  */
char *
tty_getf (const char *promptfmt, ... )
{
  va_list arg_ptr;
  char *prompt;
  char *answer;

  va_start (arg_ptr, promptfmt);
  if (gpgrt_vasprintf (&prompt, promptfmt, arg_ptr) < 0)
    log_fatal ("estream_vasprintf failed: %s\n", strerror (errno));
  va_end (arg_ptr);
  answer = tty_get (prompt);
  xfree (prompt);
  return answer;
}


char *
tty_get_hidden( const char *prompt )
{
  return do_get (prompt, 1);
}


void
tty_kill_prompt (void)
{
  if (no_terminal)
    return;

  if (!initialized)
    init_ttyfp ();

  if (batchmode)
    last_prompt_len = 0;
  if (!last_prompt_len)
    return;
#ifdef HAVE_W32_SYSTEM
  tty_printf ("\r%*s\r", last_prompt_len, "");
#else /* Unix */
  {
    int i;
    putc ('\r', ttyfp);
    for (i=0; i < last_prompt_len; i ++ )
      putc (' ', ttyfp);
    putc ('\r', ttyfp);
    fflush (ttyfp);
  }
#endif /* Unix */
  last_prompt_len = 0;
}


int
tty_get_answer_is_yes( const char *prompt )
{
  int yes;
  char *p;

  p = tty_get (prompt);
  tty_kill_prompt ();
  yes = answer_is_yes (p);
  xfree (p);

  return yes;
}


/* Called by gnupg_rl_initialize to setup the readline support. */
void
tty_private_set_rl_hooks (void (*init_stream) (FILE *),
                          void (*set_completer) (rl_completion_func_t*),
                          void (*inhibit_completion) (int),
                          void (*cleanup_after_signal) (void),
                          char *(*readline_fun) (const char*),
                          void (*add_history_fun) (const char*),
                          int (*rw_history_fun)(const char *, int, int))
{
  my_rl_init_stream = init_stream;
  my_rl_set_completer = set_completer;
  my_rl_inhibit_completion = inhibit_completion;
  my_rl_cleanup_after_signal = cleanup_after_signal;
  my_rl_readline = readline_fun;
  my_rl_add_history = add_history_fun;
  my_rl_rw_history = rw_history_fun;
}


/* Read the history from FILENAME or limit the size of the history.
 * If FILENAME is NULL and NLINES is zero the current history is
 * cleared.  Returns 0 on success or -1 on error and sets ERRNO.  No
 * error is return if readline support is not available.  */
int
tty_read_history (const char *filename, int nlines)
{
  int rc;

  if (!my_rl_rw_history)
    return 0;

  rc = my_rl_rw_history (filename, 0, nlines);
  if (rc && gpg_err_code_from_syserror () == GPG_ERR_ENOENT)
    rc = 0;

  return rc;
}


/* Write the current history to the file FILENAME.  Returns 0 on
 * success or -1 on error and sets ERRNO.  No error is return if
 * readline support is not available.  */
int
tty_write_history (const char *filename)
{
  if (!my_rl_rw_history)
    return 0;

  return my_rl_rw_history (filename, 1, 0);
}


#ifdef HAVE_LIBREADLINE
void
tty_enable_completion (rl_completion_func_t *completer)
{
  if (no_terminal || !my_rl_set_completer )
    return;

  if (!initialized)
    init_ttyfp();

  my_rl_set_completer (completer);
}

void
tty_disable_completion (void)
{
  if (no_terminal || !my_rl_inhibit_completion)
    return;

  if (!initialized)
    init_ttyfp();

  my_rl_inhibit_completion (1);
}
#endif /* HAVE_LIBREADLINE */

void
tty_cleanup_after_signal (void)
{
#ifdef HAVE_TCGETATTR
  cleanup ();
#endif
}

void
tty_cleanup_rl_after_signal (void)
{
  if (my_rl_cleanup_after_signal)
    my_rl_cleanup_after_signal ();
}
